// HTMLParser Library - A java-based parser for HTML
// http://htmlparser.org
// Copyright (C) 2006 Derrick Oswald
//
// Revision Control Information
//
// $URL: https://svn.sourceforge.net/svnroot/htmlparser/trunk/filterbuilder/src/main/java/org/htmlparser/parserapplications/filterbuilder/FilterBuilder.java $
// $Author: derrickoswald $
// $Date: 2006-09-17 21:02:25 -0400 (Sun, 17 Sep 2006) $
// $Revision: 8 $
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the Common Public License; either
// version 1.0 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// Common Public License for more details.
//
// You should have received a copy of the Common Public License
// along with this library; if not, the license is available from
// the Open Source Initiative (OSI) website:
//   http://opensource.org/licenses/cpl1.0.php

package org.htmlparser.parserapplications.filterbuilder;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Event;
import java.awt.FileDialog;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetContext;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.beans.PropertyVetoException;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Vector;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JDesktopPane;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSplitPane;
import javax.swing.JTextField; //import javax.swing.JTextPane;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.ScrollPaneConstants;
import javax.swing.WindowConstants;

import org.htmlparser.Parser;
import org.htmlparser.beans.FilterBean;
import org.htmlparser.parserapplications.filterbuilder.layouts.NullLayoutManager;
import org.htmlparser.util.EncodingChangeException;
import org.htmlparser.util.NodeIterator;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;

/**
 * The main program for the FilterBuilder programming system.
 * <p>
 * ToDo:
 * <ul>
 * <li>thread the attribute fetching</li>
 * <li>CSS selector filter</li>
 * <li>table row filter</li>
 * <li>table column filter</li>
 * <li>trigger filter</li>
 * <li>undo</li>
 * <li>handle bad URLs</li>
 * <li>StringBean type secondary text output</li>
 * <li>context sensitive menus</li>
 * </ul>
 */
public class FilterBuilder extends JFrame implements WindowListener, ActionListener, MouseListener, MouseMotionListener, DragGestureListener, DragSourceListener, DropTargetListener, ClipboardOwner {
	static final String TITLE = "HTML Parser FilterBuilder";

	static final URL mDocumentBase;

	static {

		String p;
		char ps;
		URL base;

		p = System.getProperty("user.dir");
		// if the system file separator isn't the URL file separator convert it.
		try {
			ps = (System.getProperty("file.separator")).charAt(0);
			if ('/' != ps)
				p.replace(ps, '/');
		} catch (StringIndexOutOfBoundsException e) {
		}

		try {
			base = new URL("file:///" + p + "/");
		} catch (MalformedURLException murle) {
			base = null;
		}
		mDocumentBase = base;
	}

	static String mHomeDir;

	static {
		String dir;
		File file;

		dir = System.getProperty("user.home") + System.getProperty("file.separator") + ".htmlparser";
		file = new File(dir);
		if (!file.exists())
			if (!file.mkdirs()) // make the directory if it doesn't exist
				throw new RuntimeException("cannot create directory " + file.getAbsolutePath());
		mHomeDir = file.getAbsolutePath();
	}

	/**
	 * The relative position of the mouse while dragging.
	 */
	protected Point mBasePoint;

	/**
	 * Selected commands.
	 */
	protected Vector mSelection;

	/**
	 * If true selection moved.
	 */
	protected boolean mMoved;

	/**
	 * This component is a drop target.
	 */
	protected DropTarget mDropTarget;

	/**
	 * Enables this component to be a Drag Source.
	 */
	protected DragSource mDragSource;

	/**
	 * Kludge: Used by actionPerformed/filterAction to remember the filter menu
	 * item.
	 */
	protected Component mCurrentComponent;

	/**
	 * The main panel GUI component.
	 */
	protected JPanel mMainPanel;

	/**
	 * The main panel scrolling GUI component.
	 */
	protected JScrollPane mMainScroller;

	/**
	 * The URL input GUI component.
	 */
	protected JTextField mURLField;

	/**
	 * The output panel GUI component.
	 */
	protected JDesktopPane mOutput;

	/**
	 * Create an FilterBuilder programming environment.
	 */
	public FilterBuilder() {
		JMenuBar menubar;
		JToolBar toolbar;
		JMenu menu;
		JPanel panel;
		JScrollPane pane;
		JSplitPane split;
		JMenuItem item;

		// drag and drop support
		mMainPanel = new JPanel();
		mDropTarget = new DropTarget(mMainPanel, this);
		mDragSource = new DragSource();

		// menu and toolbar
		menubar = new JMenuBar();
		toolbar = new JToolBar();
		toolbar.setAlignmentY(0.222222F);

		// file menu
		menu = new JMenu();
		menu.setText("File");
		menu.setActionCommand("File");
		menu.setMnemonic((int) 'F');
		makeMenuButton("New", "Create a new document", "New", 'N', KeyStroke.getKeyStroke(KeyEvent.VK_N, Event.CTRL_MASK), toolbar, menu);
		makeMenuButton("Open", "Open an existing document", "Open...", 'O', KeyStroke.getKeyStroke(KeyEvent.VK_O, Event.CTRL_MASK), toolbar, menu);
		makeMenuButton("Save", "Save the active document", "Save...", 'S', KeyStroke.getKeyStroke(KeyEvent.VK_S, Event.CTRL_MASK), toolbar, menu);
		makeMenuButton("SaveAs", "Save the active document", "Save As...", 'A', KeyStroke.getKeyStroke(KeyEvent.VK_A, Event.CTRL_MASK), null, menu);
		menu.add(new JSeparator());
		makeMenuButton("Exit", "Exit the program", "Exit", 'E', KeyStroke.getKeyStroke(KeyEvent.VK_E, Event.CTRL_MASK), null, menu);
		menubar.add(menu);

		toolbar.add(new JToolBar.Separator());

		// edit menu
		menu = new JMenu();
		menu.setText("Edit");
		menu.setActionCommand("Edit");
		menu.setMnemonic((int) 'E');
		makeMenuButton("Cut", "Cut the selection and put it on the Clipboard", "Cut", 'T', KeyStroke.getKeyStroke(KeyEvent.VK_X, Event.CTRL_MASK), toolbar, menu);
		makeMenuButton("Copy", "Copy the selection and put it on the Clipboard", "Copy", 'C', KeyStroke.getKeyStroke(KeyEvent.VK_C, Event.CTRL_MASK), toolbar, menu);
		makeMenuButton("Paste", "Insert Clipboard contents", "Paste", 'P', KeyStroke.getKeyStroke(KeyEvent.VK_V, Event.CTRL_MASK), toolbar, menu);
		makeMenuButton("Delete", "Delete the selection", "Delete", 'D', KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), toolbar, menu);
		menubar.add(menu);

		// filter menu
		menu = new JMenu();
		menu.setText("Filter");
		menu.setActionCommand("Filter");
		menu.setMnemonic((int) 'F');
		menubar.add(menu);

		toolbar.add(new JToolBar.Separator());

		// filters menu and filters toolbar
		addFilter(menu, toolbar, "org.htmlparser.parserapplications.filterbuilder.wrappers.AndFilterWrapper");
		addFilter(menu, toolbar, "org.htmlparser.parserapplications.filterbuilder.wrappers.OrFilterWrapper");
		addFilter(menu, toolbar, "org.htmlparser.parserapplications.filterbuilder.wrappers.NotFilterWrapper");
		menu.addSeparator();
		toolbar.add(new JToolBar.Separator());

		addFilter(menu, toolbar, "org.htmlparser.parserapplications.filterbuilder.wrappers.StringFilterWrapper");
		addFilter(menu, toolbar, "org.htmlparser.parserapplications.filterbuilder.wrappers.RegexFilterWrapper");
		addFilter(menu, toolbar, "org.htmlparser.parserapplications.filterbuilder.wrappers.TagNameFilterWrapper");
		addFilter(menu, toolbar, "org.htmlparser.parserapplications.filterbuilder.wrappers.NodeClassFilterWrapper");
		addFilter(menu, toolbar, "org.htmlparser.parserapplications.filterbuilder.wrappers.HasAttributeFilterWrapper");
		menu.addSeparator();
		toolbar.add(new JToolBar.Separator());

		addFilter(menu, toolbar, "org.htmlparser.parserapplications.filterbuilder.wrappers.HasParentFilterWrapper");
		addFilter(menu, toolbar, "org.htmlparser.parserapplications.filterbuilder.wrappers.HasChildFilterWrapper");
		addFilter(menu, toolbar, "org.htmlparser.parserapplications.filterbuilder.wrappers.HasSiblingFilterWrapper");
		menu.addSeparator();
		toolbar.add(new JToolBar.Separator());

		// operation menu
		menu = new JMenu();
		menu.setText("Operation");
		menu.setActionCommand("Operation");
		menu.setMnemonic((int) 'r');
		item = new JMenuItem();
		item.setText("Expand");
		item.setActionCommand("expandAction");
		item.addActionListener(this);
		menu.add(item);
		item = new JMenuItem();
		item.setText("Collapse");
		item.setActionCommand("collapseAction");
		item.addActionListener(this);
		menu.add(item);
		menu.addSeparator();
		item = new JMenuItem();
		item.setText("Expand All");
		item.setActionCommand("expandAllAction");
		item.addActionListener(this);
		menu.add(item);
		item = new JMenuItem();
		item.setText("Collapse All");
		item.setActionCommand("collapseAllAction");
		item.addActionListener(this);
		menu.add(item);
		menu.addSeparator();
		item = new JMenuItem("Fetch Page");
		item.setActionCommand("fetchAction");
		item.addActionListener(this);
		menu.add(item);
		item = new JMenuItem("Execute Filter");
		item.setActionCommand("executeAction");
		item.addActionListener(this);
		menu.add(item);
		menubar.add(menu);

		// help menu
		menu = new JMenu();
		menu.setText("Help");
		menu.setActionCommand("Help");
		menu.setMnemonic((int) 'H');
		item = new JMenuItem("Filtering");
		item.setActionCommand("filteringAction");
		item.addActionListener(this);
		menu.add(item);
		item = new JMenuItem("Instructions");
		item.setActionCommand("instructionsAction");
		item.addActionListener(this);
		menu.add(item);
		item = new JMenuItem("Tutorial");
		item.setActionCommand("tutorialAction");
		item.addActionListener(this);
		menu.add(item);
		item = new JMenuItem("Hints");
		item.setActionCommand("hintsAction");
		item.addActionListener(this);
		menu.add(item);
		makeMenuButton("About", "Display program information, version number and copyright", "About", 'B', KeyStroke.getKeyStroke(KeyEvent.VK_H, Event.CTRL_MASK), toolbar, menu);
		menubar.add(menu);

		setJMenuBar(menubar);

		// toolbar panel
		panel = new JPanel();
		panel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
		panel.add(toolbar);
		getContentPane().setLayout(new BorderLayout(0, 0));
		getContentPane().add(BorderLayout.NORTH, panel);

		// URL entry
		mURLField = new JTextField();
		mURLField.setToolTipText("Enter the URL to view");
		// mTextField.addActionListener (this);
		mURLField.setText("http://sourceforge.org/projects/htmlparser");
		getContentPane().add(BorderLayout.SOUTH, mURLField);

		// application setup
		setTitle(TITLE);
		setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
		setSize(640, 480);
		setVisible(false);

		// main panel
		mMainPanel.setLayout(new NullLayoutManager());
		mMainScroller = new JScrollPane(mMainPanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);

		split = new JSplitPane();
		pane = new JScrollPane();
		pane.setViewportView(mMainScroller);
		split.setLeftComponent(pane);

		mOutput = new JDesktopPane();
		split.setRightComponent(mOutput);

		getContentPane().add(BorderLayout.CENTER, split);

		// shenanigans to get the splitter bar at the midpoint
		setVisible(true);
		split.setDividerLocation(0.5);
		setVisible(false);

		// listeners
		addWindowListener(this);
		setIconImage(Toolkit.getDefaultToolkit().getImage("images/program16.gif"));
		addMouseListener(this);
		addMouseMotionListener(this);

		// clipboard buffer
		mSelection = new Vector();
	}

	/**
	 * Creates a new instance of an FilterBuilder environment with the given
	 * title.
	 * 
	 * @param title
	 *            the title for the new frame.
	 * @see #FilterBuilder()
	 */
	public FilterBuilder(String title) {
		this();
		setTitle(title);
	}

	/**
	 * Makes menu and toolbar items for commands.
	 * 
	 * @param name
	 *            The name of the command.
	 * @param description
	 *            A description for the tooltip.
	 * @param text
	 *            The text for the menu.
	 * @param mnemonic
	 *            The navigation mnemonic.
	 * @param key
	 *            Accelerator key.
	 * @param toolbar
	 *            The toolbar to add the button to.
	 * @param menu
	 *            The menu to add the menu item to.
	 */
	protected void makeMenuButton(String name, String description, String text, int mnemonic, KeyStroke key, JToolBar toolbar, JMenu menu) {
		JButton button;
		JMenuItem item;
		ImageIcon icon;
		String command;

		command = name.toLowerCase();
		try {
			icon = new ImageIcon(getURL("images/" + command + ".gif"));
		} catch (java.net.MalformedURLException error) {
			icon = null;
		}

		item = new JMenuItem();
		item.setText(text);
		item.setActionCommand(command + "Action");
		item.setAccelerator(key);
		item.setMnemonic(mnemonic);
		item.setIcon(icon);
		item.addActionListener(this);
		menu.add(item);

		if (null != toolbar) {
			button = new JButton();
			button.setDefaultCapable(false);
			button.setToolTipText(description);
			button.setMnemonic(mnemonic);
			button.setActionCommand(command + "Action");
			button.setMargin(new Insets(0, 0, 0, 0));
			button.setIcon(icon);
			button.addActionListener(this);
			toolbar.add(button);
		}
	}

	/**
	 * Get a url for the given resource specification.
	 * 
	 * @param spec
	 *            The name of the resource.
	 * @return The fully formed URL.
	 * @exception MalformedURLException
	 *                In the case that the document base or name of the resource
	 *                cannot be turned into a URL.
	 */
	protected URL getURL(String spec) throws MalformedURLException {
		URL ret;

		if (null == (ret = getClass().getResource(spec)))
			if ((null != mDocumentBase) && (-1 == spec.indexOf("//")))
				ret = new URL(mDocumentBase, spec);
			else
				ret = new URL(spec);

		return ret;
	}

	/**
	 * Creates a new button for the given class.
	 * 
	 * @param class_name
	 *            The name of the Filter class.
	 * @return A fully functional button with name, tool tip, icon and drag
	 *         recognizer.
	 */
	public JButton makeFilterButton(String class_name) {
		Filter filter;
		JButton ret;

		ret = new JButton();
		filter = Filter.instantiate(class_name);
		if (null != filter) {
			ret.setName(class_name); // filter.getNodeFilter ().getClass
										// ().getName ());
			ret.setToolTipText(filter.getDescription());
			ret.setMargin(new Insets(0, 0, 0, 0));
			ret.setIcon(filter.getIcon());
			mDragSource.createDefaultDragGestureRecognizer(ret, DnDConstants.ACTION_MOVE, this);
			ret.setActionCommand("filterAction");
			ret.addActionListener(this);
		}

		return (ret);
	}

	/**
	 * Add a filter to the GUI. Adds the filter specified by class_name to the
	 * menu, toolbar and starts listening for actions.
	 * 
	 * @param menu
	 *            The menu to add the filter to.
	 * @param toolbar
	 *            The toolbar to add the filter to.
	 * @param class_name
	 *            The class name for the filter wrapper. From the wrapper, the
	 *            NodeFilter, description and icon can be obtained.
	 */
	public void addFilter(JMenu menu, JToolBar toolbar, String class_name) {
		Filter filter;

		filter = Filter.instantiate(class_name);
		if (null != filter) {
			String name;
			String description;
			Icon icon;
			String text;
			JMenuItem item;

			name = filter.getNodeFilter().getClass().getName();
			description = filter.getDescription();
			icon = filter.getIcon();
			text = name.substring(name.lastIndexOf('.') + 1);

			item = new JMenuItem();
			item.setName(class_name);
			item.setText(text);
			item.setActionCommand("filterAction");
			// item.setAccelerator (key);
			// item.setMnemonic (mnemonic);
			item.setToolTipText(description);
			item.setIcon(icon);
			item.addActionListener(this);
			menu.add(item);

			toolbar.add(makeFilterButton(class_name));
		}
	}

	/**
	 * Adds a set of filters to the main panel or a sublist. Sets up the GUI
	 * components as drop targets and mouse listeners, and performs a relayout
	 * to display them.
	 * 
	 * @param filters
	 *            The filter wrappers to add.
	 * @param point
	 *            The point at which to start adding (list == null).
	 * @param list
	 *            The list to add to (point not used), or <code>null</code>
	 *            for the main panel.
	 */
	protected void insertFilters(Filter[] filters, Point point, SubFilterList list) {
		Dimension dimension;

		if (null == list) {
			for (int i = 0; i < filters.length; i++) {
				filters[i].setLocation(point);
				mMainPanel.add(filters[i]);
				dimension = filters[i].getPreferredSize();
				point.y += dimension.height;
			}
		} else
			for (int i = 0; i < filters.length; i++)
				list.addFilter(filters[i]);
		setupDropTargets(filters);
		setupMouseListeners(filters);
		relayout();
	}

	/**
	 * Sets the position of the mouse in the component.
	 * 
	 * @param point
	 *            The point where the mouse position is.
	 */
	protected void setBasePoint(Point point) {
		mBasePoint = point;
	}

	/**
	 * Gets the current base point of the mouse pointer. This value is used to
	 * offset the drag position to maintain the mouse position at the same
	 * relative position within the card while dragging.
	 * 
	 * @return The current base point of the mouse pointer.
	 */
	protected Point getBasePoint() {
		return (mBasePoint);
	}

	/**
	 * Get the enclosing sub filter list if any.
	 * 
	 * @param component
	 *            The component that's supposedly enclosed.
	 * @return The enclosing component or <code>null</code> otherwise.
	 */
	protected SubFilterList getEnclosing(Component component) {
		do
			component = component.getParent();
		while ((null != component) && !(component instanceof SubFilterList));

		return ((SubFilterList) component);
	}

	/**
	 * Get the enclosed sub filter list if any.
	 * 
	 * @param component
	 *            The component that's supposedly enclosing the list.
	 * @return The enclosed component or <code>null</code> otherwise.
	 */
	protected SubFilterList getEnclosed(Component component) {
		Component[] list;

		if (component instanceof Container) {
			list = ((Container) component).getComponents();
			for (int i = 0; i < list.length; i++)
				if (list[i] instanceof SubFilterList)
					return ((SubFilterList) list[i]);
		}

		return (null);
	}

	/**
	 * Makes a program like:
	 * 
	 * <pre>
	 * // Generated by FilterBuilder. http://htmlparser.org
	 * // [aced0005737200206f72672e68746d6c7061727365722e66696c746572732e416e6446696c74657224c30516b2b7b2120200015b000b6d5072656469636174657374001c5b4c6f72672f68746d6c7061727365722f4e6f646546696c7465723b78707572001c5b4c6f72672e68746d6c7061727365722e4e6f646546696c7465723b8f17479b1d5f7992020000787000000002737200246f72672e68746d6c7061727365722e66696c746572732e5461674e616d6546696c746572b28b2601a614890f0200014c00056d4e616d657400124c6a6176612f6c616e672f537472696e673b78707400044d455441737200296f72672e68746d6c7061727365722e66696c746572732e48617341747472696275746546696c74657296abdfb3b0714cda0200024c000a6d41747472696275746571007e00064c00066d56616c756571007e000678707400046e616d6570]
	 * 
	 * import org.htmlparser.*;
	 * import org.htmlparser.filters.*;
	 * import org.htmlparser.beans.*;
	 * import org.htmlparser.util.*;
	 * 
	 * public class Test {
	 * 	public static void main(String args[]) {
	 * 		TagNameFilter filter0 = new TagNameFilter();
	 * 		filter0.setName(&quot;META&quot;);
	 * 		HasAttributeFilter filter1 = new HasAttributeFilter();
	 * 		filter1.setAttributeName(&quot;name&quot;);
	 * 		NodeFilter[] array0 = new NodeFilter[2];
	 * 		array0[0] = filter0;
	 * 		array0[1] = filter1;
	 * 		AndFilter filter2 = new AndFilter();
	 * 		filter2.setPredicates(array0);
	 * 		NodeFilter[] array1 = new NodeFilter[1];
	 * 		array1[0] = filter2;
	 * 		FilterBean bean = new FilterBean();
	 * 		bean.setFilters(array1);
	 * 		if (0 != args.length) {
	 * 			bean.setURL(args[0]);
	 * 			System.out.println(bean.getNodes().toHtml());
	 * 		} else
	 * 			System.out.println(&quot;Usage: java -classpath .:htmlparser.jar:htmllexer.jar Test &lt;url&gt;&quot;);
	 * 	}
	 * }
	 * </pre>
	 * 
	 * @param name
	 *            The name of the class.
	 * @param out
	 *            The buffer to append to.
	 * @param bean
	 *            The bean to extract the filters from to make the program.
	 */
	protected void makeProgram(String name, StringBuffer out, FilterBean bean) {
		// so we need to keep track of filters and arrays of filters to give
		// them unique numbers
		// each Filter is responsible for outputting it's code and returning
		// it's variable name
		int[] context; // 0 - indent, 1 - next filter variable #, 2 - next
						// array of filters variable #
		String[] names;
		Filter[] filters;
		String array;

		filters = (Filter[]) bean.getFilters();

		context = new int[3];
		context[0] = 0;

		Filter.spaces(out, context[0]);
		out.append("// Generated by FilterBuilder. http://htmlparser.org");
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("// ");
		try {
			out.append(Filter.deconstitute(filters));
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
		Filter.newline(out);
		Filter.newline(out);

		Filter.spaces(out, context[0]);
		out.append("import org.htmlparser.*;");
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("import org.htmlparser.filters.*;");
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("import org.htmlparser.beans.*;");
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("import org.htmlparser.util.*;");
		Filter.newline(out);
		Filter.newline(out);

		Filter.spaces(out, context[0]);
		out.append("public class ");
		out.append(name);
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("{");

		context[0] = 4;
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("public static void main (String args[])");
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("{");
		Filter.newline(out);

		context[0] = 8;
		names = new String[filters.length];
		for (int i = 0; i < names.length; i++)
			names[i] = filters[i].toJavaCode(out, context);

		array = "array" + context[2]++;
		Filter.spaces(out, context[0]);
		out.append("NodeFilter[] ");
		out.append(array);
		out.append(" = new NodeFilter[");
		out.append(filters.length);
		out.append("];");
		Filter.newline(out);
		for (int i = 0; i < filters.length; i++) {
			Filter.spaces(out, context[0]);
			out.append(array);
			out.append("[");
			out.append(i);
			out.append("] = ");
			out.append(names[i]);
			out.append(";");
			Filter.newline(out);
		}

		Filter.spaces(out, context[0]);
		out.append("FilterBean bean = new FilterBean ();");
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("bean.setFilters (");
		out.append(array);
		out.append(");");
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("if (0 != args.length)");
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("{");
		Filter.newline(out);
		context[0] = 12;
		Filter.spaces(out, context[0]);
		out.append("bean.setURL (args[0]);");
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("System.out.println (bean.getNodes ().toHtml ());");
		Filter.newline(out);
		context[0] = 8;
		Filter.spaces(out, context[0]);
		out.append("}");
		Filter.newline(out);
		Filter.spaces(out, context[0]);
		out.append("else");
		Filter.newline(out);
		context[0] = 12;
		Filter.spaces(out, context[0]);
		String sep = System.getProperty("path.separator");
		out.append("System.out.println (\"Usage: java -classpath ." + sep + "htmlparser.jar" + sep + "htmllexer.jar ");
		out.append(name);
		out.append(" <url>\");");
		Filter.newline(out);

		context[0] = 4;
		Filter.spaces(out, context[0]);
		out.append("}");
		Filter.newline(out);

		context[0] = 0;
		Filter.spaces(out, context[0]);
		out.append("}");
		Filter.newline(out);
	}

	/**
	 * Extracts a java class name from a file name. ToDo: make this
	 * package-smart somehow.
	 * 
	 * @param file
	 *            The name of the file.
	 * @return The name of the class.
	 */
	protected String classFromFile(String file) {
		String filesep;
		int index;

		// remove any path
		filesep = System.getProperty("file.separator");
		index = file.lastIndexOf(filesep);
		if (-1 != index)
			file = file.substring(index + filesep.length());
		// remove the extension
		index = file.indexOf('.');
		if (-1 != index)
			file = file.substring(0, index);

		return (file);
	}

	/**
	 * Save the workspace contents to file.
	 * 
	 * @param name
	 *            The name of the file to save it under.
	 */
	public void save(String name) {
		Filter[] selections;
		FilterBean bean;
		StringBuffer buffer;
		PrintWriter out;
		String ok = "OK";

		selections = getFilters();
		if (0 != selections.length) {
			bean = new FilterBean();
			bean.setURL(mURLField.getText());
			bean.setFilters(selections);
			buffer = new StringBuffer();
			makeProgram(classFromFile(name), buffer, bean);
			try {
				out = new PrintWriter(new FileWriter(name), true);
				try {
					out.write(buffer.toString());
					out.flush();
				} finally {
					out.close();
				}
			} catch (IOException ioe) {
				ioe.printStackTrace();
			}
		} else
			// ToDo: grey out save option if nothing to save...
			JOptionPane.showOptionDialog(mMainPanel, "No filters to save.", "Oops", JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, null, new String[] { ok }, ok);
	}

	/**
	 * The action to take when "New" menu or button pressed.
	 */
	protected void newAction() {
		mMainPanel.removeAll();
		mSelection.clear();
		relayout();
	}

	/**
	 * The action to take when "Open" menu or button pressed.
	 */
	protected void openAction() {
		FileDialog dialog;
		File file;

		dialog = new FileDialog(this);
		dialog.setMode(FileDialog.LOAD);
		dialog.setTitle("Open");
		dialog.setDirectory(mHomeDir);
		dialog.setVisible(true);
		if (null != dialog.getFile()) {
			mHomeDir = dialog.getDirectory();
			file = new File(mHomeDir + dialog.getFile());
			open(file.getAbsolutePath());
			setTitle(TITLE + " - " + file.getAbsolutePath());
		}
	}

	/**
	 * The action to take when "Save" menu or button pressed.
	 */
	protected void saveAction() {
		String title;
		int index;
		File file;
		FileDialog dialog;

		title = getTitle();
		index = title.indexOf(" - ");
		if (-1 != index)
			file = new File(title.substring(index + 3));
		else {
			dialog = new FileDialog(this);
			dialog.setMode(FileDialog.SAVE);
			dialog.setTitle("Save");
			dialog.setDirectory(mHomeDir);
			dialog.setVisible(true);
			if (null != dialog.getFile()) {
				mHomeDir = dialog.getDirectory();
				file = new File(mHomeDir + dialog.getFile());
				setTitle(TITLE + " - " + file.getAbsolutePath());
			} else
				file = null;
		}
		if (null != file)
			save(file.getAbsolutePath());
	}

	/**
	 * The action to take when "Save As" menu or button pressed.
	 */
	protected void saveasAction() {
		setTitle(TITLE);
		saveAction();
	}

	/**
	 * The action to take when "Exit" menu or button pressed.
	 */
	protected void exitAction() {
		exitApplication();
	}

	/**
	 * The action to take when "Cut" menu or button pressed.
	 */
	protected void cutAction() {
		String string;
		StringSelection contents;
		Clipboard cb;

		// get the selection
		string = serializeSelection();
		// copy to clipboard
		contents = new StringSelection(string);
		cb = Toolkit.getDefaultToolkit().getSystemClipboard();
		cb.setContents(contents, this);
		// delete the selection
		deleteSelection();
		relayout();
	}

	/**
	 * The action to take when "Copy" menu or button pressed.
	 */
	protected void copyAction() {
		String string;
		StringSelection contents;
		Clipboard cb;

		// get the selection
		string = serializeSelection();
		// copy to clipboard
		contents = new StringSelection(string);
		cb = Toolkit.getDefaultToolkit().getSystemClipboard();
		cb.setContents(contents, this);
	}

	/**
	 * The action to take when "Paste" menu or button pressed.
	 */
	protected void pasteAction() {
		Clipboard cb;
		Transferable content;
		String string;
		Filter[] filters;
		Point point;
		SubFilterList list;

		// get the text
		cb = Toolkit.getDefaultToolkit().getSystemClipboard();
		content = cb.getContents(this);
		if (content.isDataFlavorSupported(DataFlavor.stringFlavor)) {
			try {
				string = (String) content.getTransferData(DataFlavor.stringFlavor);
				// deserialize it and add into the selection
				filters = Filter.reconstitute(string, new Parser(mURLField.getText()));
				// add it to the (single) selected object or main panel
				if (isSingleSelection() && (null != (list = getEnclosed(getSelection()[0])))) {
					for (int i = 0; i < filters.length; i++)
						list.addFilter(filters[i]);
				} else {
					point = new Point(0, 0);
					for (int i = 0; i < filters.length; i++) {
						filters[i].setLocation(point);
						mMainPanel.add(filters[i]);
						point.y += filters[i].getPreferredSize().height;
					}
				}
				setupMouseListeners(filters);
				setupDropTargets(filters);
				relayout();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * The action to take when "Delete" menu or button pressed.
	 */
	protected void deleteAction() {
		// delete the selection
		deleteSelection();
		relayout();
	}

	/**
	 * The action to take when a filter menu or button pressed.
	 */
	protected void filterAction() {
		String cls;
		Filter filter;
		SubFilterList list;
		Point point;

		// retrieve the source component placed there by actionPerformed
		cls = mCurrentComponent.getName();
		filter = Filter.instantiate(cls);
		// need this to get the underlying filter prepped?
		try {
			filter = Filter.wrap(filter.getNodeFilter(), new Parser(mURLField.getText()));
		} catch (ParserException pe) {
			pe.printStackTrace();
		}
		// add it to the (single) selected object or main panel
		if (isSingleSelection() && (null != (list = getEnclosed(getSelection()[0])))) {
			insertFilters(new Filter[] { filter }, null, list);
		} else {
			point = new Point(50, 50); // find where and who to stick it into
			insertFilters(new Filter[] { filter }, point, null);
		}
	}

	/**
	 * The action to take when "Fetch" menu pressed.
	 */
	protected void fetchAction() {
		JInternalFrame frame;
		Dimension dimension;
		Parser parser;
		NodeList list;

		// set up an internal frame for the results
		frame = new JInternalFrame(mURLField.getText());
		frame.setClosable(true);
		frame.setResizable(true);
		dimension = mOutput.getSize();
		frame.setBounds(0, 0, dimension.width, dimension.height);
		list = new NodeList();
		try {
			parser = new Parser(mURLField.getText());
			try {
				for (NodeIterator iterator = parser.elements(); iterator.hasMoreNodes();)
					list.add(iterator.nextNode());
			} catch (EncodingChangeException ece) {
				list.removeAll();
				parser.reset();
				for (NodeIterator iterator = parser.elements(); iterator.hasMoreNodes();)
					list.add(iterator.nextNode());
			}
		} catch (ParserException pe) {
			pe.printStackTrace();
		}
		JTree tree = new JTree(new HtmlTreeModel(list));
		tree.setRootVisible(false);
		tree.setCellRenderer(new HtmlTreeCellRenderer());
		JScrollPane treeView = new JScrollPane(tree);
		frame.setContentPane(new JScrollPane(treeView, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
		mOutput.add(frame, new Integer(1));
		try {
			frame.setSelected(true);
		} catch (PropertyVetoException pve) {
			pve.printStackTrace();
		}
		frame.show();
	}

	/**
	 * The action to take when "Execute" menu or button pressed.
	 */
	protected void executeAction() {
		Filter[] selections;
		FilterBean bean;
		JInternalFrame frame;
		Dimension dimension;
		// JTextPane text;

		selections = getSelection();
		if (0 == selections.length)
			selections = getFilters();
		if (0 != selections.length) {
			bean = new FilterBean();
			bean.setURL(mURLField.getText());
			bean.setFilters(selections);

			// set up an internal frame for the results
			frame = new JInternalFrame(bean.getURL());
			frame.setClosable(true);
			frame.setResizable(true);
			dimension = mOutput.getSize();
			frame.setBounds(0, 0, dimension.width, dimension.height / 2);
			JTree tree = new JTree(new HtmlTreeModel(bean.getNodes()));
			tree.setRootVisible(false);
			tree.setCellRenderer(new HtmlTreeCellRenderer());
			JScrollPane treeView = new JScrollPane(tree);
			frame.setContentPane(new JScrollPane(treeView, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
			// text = new JTextPane ();
			// text.setText (bean.getNodes ().toHtml ());
			// frame.setContentPane (new JScrollPane (
			// text,
			// ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
			// ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
			mOutput.add(frame, new Integer(2)); // layer 2?
			try {
				frame.setSelected(true);
			} catch (PropertyVetoException pve) {
				pve.printStackTrace();
			}
			frame.show();
		}
	}

	/**
	 * The action to take when "Instructions" menu pressed.
	 */
	protected void instructionsAction() {
		String instructions = "<html>" + "Enter the target URL in the text box at the bottom of the window.<br>" + "Choose 'Fetch Page' from the Operations menu to see the whole page.<br>"
				+ "Pick filters from the Filter menu or drag them from the toolbar.<br>" + "Filters such as And, Or, Not, HasParent, HasChild and HasSibling contain other filters:<br>"
				+ "<ul><li>drag new filters into their blank areas at the bottom</li>" + "<li>cut an existing filter and paste into a selected filter</li></ul>"
				+ "Build the filter incrementally, choosing 'Execute Filter' to test the selected filter.<br>" + "Save creates a .java file that runs the top level filter.<br>"
				+ "Right click on a filter displays a pop-up menu.<br>" + "Double click on a blue item in the result pane expands the tree." + "</html>";
		String close = "Close";
		JOptionPane.showOptionDialog(
		// not .showMessageDialog(
				mMainPanel, instructions, "FilterBuilder Instructons",
				// remove this:
				JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE,
				// and remove rest of these:
				null, new String[] { close }, close);
	}

	/**
	 * The action to take when "Filtering" menu pressed.
	 */
	protected void filteringAction() {
		String instructions = "<html>" + "The HTML Parser filter subsystem extracts items from a web page,<br>"
				+ "corresponding to the use-case 'I want this little piece of information from http://yadda'.<br>"
				+ "The web page is considered a heirarchical tree of nodes. Usually the root node is &lt;html&gt;,<br>" + "intermediate level nodes are &lt;div&gt; and &lt;table&gt; for example,<br>"
				+ "and leaf nodes are things like text or &lt;img&gt;.<br>" + "Any node that isn't the root node has a 'parent' node.<br>" + "Leaf nodes, by definition, have no 'children'.<br>"
				+ "A filter is a Java class that answers the simple question:<br>" + "<pre>Is this node acceptable? True or false.</pre><br>"
				+ "Some filters know the answer just by looking at the node,<br>" + "while others must ask other filters, sometimes looking up or down the node heirarchy.<br>"
				+ "<b>The FilterBuilder is a program for making other programs that use filters.</b><br>" + "By combining different types of filters, specific nodes can be isolated from the<br>"
				+ "target web page.<br>" + "The results are usually passed on to another part of the users program<br>" + "that does something useful with them.<br>"
				+ "The filters available include:<br>" + "<ul>" + "<li>AndFilter - The main 'combining' filter, answers <code>true</code> only if<br>"
				+ "all it's subfilters (predicates) are <code>true</code>.</li>" + "<li>OrFilter - A 'combining' filter that answers <code>true</code> if<br>"
				+ "any of it's subfilters (predicates) are <code>true</code>.</li>" + "<li>NotFilter - A 'reversing' filter that answers <code>true</code> if<br>"
				+ "it's subfilter (predicate) is <code>false</code>.</li>" + "<li>StringFilter - A 'leaf' filter that answers <code>true</code> if<br>"
				+ "the node is text and it contains a certain sequence of characters.<br>" + "It can be made case insensitive, but in this case a 'locale' must be<br>"
				+ "supplied as a context for upper-case conversion.</li>" + "<li>RegexFilter - A 'leaf' filter that answers <code>true</code> if<br>"
				+ "the node is text and it contains a certain pattern (regular expression).<br>" + "Regular expressions are descibed in the java.util.regex.Pattern class documentation.</li>"
				+ "<li>TagNameFilter - A filter that answers <code>true</code> if<br>" + "the node is a tag and it has a certain name,"
				+ "i.e. &lt;div&gt; would match the name <code>DIV</code>.</li>" + "<li>NodeClassFilter - A filter that answers <code>true</code> if<br>"
				+ "the node is a certain tag class. Not recommended, use TagNameFilter instead.</li>" + "<li>HasAttributeFilter - A filter that answers <code>true</code> if<br>"
				+ "the node is a tag and it has a certain attribute,<br>" + "i.e. &lt;script language=javascript&gt; would match the attribute <code>LANGUAGE</code>.<br>"
				+ "It can be further restricted to have a certain attribute value as well,<br>" + "i.e. 'javascript' in this example.</li>"
				+ "<li>HasParentFilter - A filter that answers <code>true</code> if<br>" + "the node is a child of a node that is acceptable to a certain filter.<br>"
				+ "This can be made recursive, which means the acceptable parent can be<br>" + "further up the heirarchy than just the immediate parent node.</li>"
				+ "<li>HasChildFilter - A filter that answers <code>true</code> if<br>" + "the node is a parent of a node that is acceptable to a certain filter.<br>"
				+ "This can be made recursive, which means the acceptable child can be<br>" + "further down the heirarchy than just the immediate children nodes.</li>"
				+ "<li>HasSiblingFilter - A filter that answers <code>true</code> if<br>" + "the node is a sibling (they have a common parent) of a node that is<br>"
				+ "acceptable to a certain filter.</li>" + "</ul>" + "</html>";
		String close = "Close";
		JOptionPane.showOptionDialog(
		// not .showMessageDialog(
				mMainPanel, instructions, "FilterBuilder Instructons",
				// remove this:
				JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE,
				// and remove rest of these:
				null, new String[] { close }, close);
	}

	/**
	 * The action to take when "Tutorial" menu pressed.
	 */
	protected void tutorialAction() {
		String instructions = "<html>" + "To get the title text from a page:<br>" + "<ul><li>Choose 'New' from the File menu.</li>" + "<li>Choose 'AndFilter' from the Filter menu.</li>"
				+ "<li>Select the And filter so it is highlighted.</li>" + "<li>Choose 'HasParent' from the Filter menu.</li>" + "<li>Toggle the 'Recursive' checkbox on in the HasParent filter.</li>"
				+ "<li>Select the HasParent filter so it is highlighted.</li>" + "<li>Choose 'TagName' from the Filter menu.<br>"
				+ "<i>Alternatively, you can drag the TagName filter (icon Hello-BOB)<br>" + "from the toolbar and drop inside the HasParent filter</i></li>"
				+ "<li>Choose 'TITLE' from the TagName combo-box.</li>" + "<li>Select the And filter and choose 'Execute Filter' from the<br>" + "Operations menu to test it.</li>"
				+ "<li>If there is unwanted non-text nodes in the result<br>" + "select the And filter and choose 'RegexFilter' from the Filter menu.</li>" + "<li>Test it again, as above.</li>"
				+ "<li>Choose 'Save' from the File menu and enter a filename like GetTitle.java</li>" + "<li>Compile the java file and run it.</li></ul>" + "</html>";
		String close = "Close";
		JOptionPane.showOptionDialog(
		// not .showMessageDialog(
				mMainPanel, instructions, "FilterBuilder Tutorial",
				// remove this:
				JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE,
				// and remove rest of these:
				null, new String[] { close }, close);
	}

	/**
	 * The action to take when "Hints" menu pressed.
	 */
	protected void hintsAction() {
		String instructions = "<html>" + "Hints:<br>" + "<ul><li>There is no undo yet, so save often.</li>" + "<li>Recursive HasParent and HasChild filters can be costly.</li>"
				+ "<li>RegexFilter is more expensive than StringFilter.</li>" + "<li>The order of predicates in And and Or filters matters for performance,<br>" + "put cheap tests first.</li>"
				+ "<li>The same node may show up more than once in the results,<br>" + "and at more than one nesting depth, depending on the filter used.</li>"
				+ "<li>Typing in a tag name in the TagName filter is not recommended,<br>" + "since extraneous characters can be added. Use an item from the list instead.</li></ul>" + "</html>";
		String close = "Close";
		JOptionPane.showOptionDialog(
		// not .showMessageDialog(
				mMainPanel, instructions, "FilterBuilder Hints",
				// remove this:
				JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE,
				// and remove rest of these:
				null, new String[] { close }, close);
	}

	/**
	 * The action to take when "About" menu or button pressed.
	 */
	protected void aboutAction() {
		String close = "Close";
		JOptionPane
				.showOptionDialog(
						// not .showMessageDialog(
						mMainPanel,
						"<html><center><font color=black>The HTML Parser <font color=blue><b>FilterBuilder</b></font><br><i>by Derrick Oswald</i>&nbsp;&nbsp;<b>DerrickOswald@users.sourceforge.net</b><br>http://htmlparser.org<br><br><font size=-2>Copyright &copy; 2005</font></center></html>",
						"About FilterBuilder",
						// remove this:
						JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE,
						// and remove rest of these:
						null, new String[] { close }, close);
	}

	/**
	 * The action to take when "Expand" menu chosen.
	 */
	public void expandAction() {
		setExpanded(getSelection(), true, false);
	}

	/**
	 * The action to take when "Collapse" menu chosen.
	 */
	public void collapseAction() {
		setExpanded(getSelection(), false, false);
	}

	/**
	 * The action to take when "Expand All" menu chosen.
	 */
	public void expandAllAction() {
		setExpanded(getSelection(), true, true);
	}

	/**
	 * The action to take when "Collapse" menu chosen.
	 */
	public void collapseAllAction() {
		setExpanded(getSelection(), false, true);
	}

	/**
	 * Set up mouse listeners. Sets <code>this</code> up to listen to each
	 * command in the list as a MouseListener. Recursively descends the tree
	 * adding to all contained elements also.
	 * 
	 * @param filters
	 *            The container with commands in it.
	 */
	public void setupMouseListeners(Filter[] filters) {
		SubFilterList list;

		for (int i = 0; i < filters.length; i++) {
			// set us up as a mouse listener on it
			((Component) filters[i]).addMouseListener(this);
			((Component) filters[i]).addMouseMotionListener(this);
			list = getEnclosed(filters[i]);
			if (null != list)
				setupMouseListeners(list.getFilters());
		}
	}

	/**
	 * Set up drop targets. Recursively descends the filter tree and sets up the
	 * filter lists as drop targets.
	 * 
	 * @param filters
	 *            The container with filters in it.
	 */
	public void setupDropTargets(Filter[] filters) {
		SubFilterList list;
		Component[] components;

		for (int i = 0; i < filters.length; i++) {
			list = getEnclosed(filters[i]);
			if (null != list) {
				components = list.getDropTargets();
				for (int j = 0; j < components.length; j++)
					new DropTarget(components[j], this);
				setupDropTargets(list.getFilters());
			}
		}
	}

	/**
	 * Expand or collapse filters, possibly recursively.
	 * 
	 * @param filters
	 *            The list of filters to expand or collapse.
	 * @param expanded
	 *            If <code>true</code> the filters are expanded, otherwise
	 *            they are collapsed.
	 * @param recursive
	 *            If <code>true</code> the filters are processed recursively.
	 */
	public void setExpanded(Filter[] filters, boolean expanded, boolean recursive) {
		SubFilterList list;

		for (int i = 0; i < filters.length; i++) {
			if (recursive && (null != (list = getEnclosed(filters[i]))))
				setExpanded(list.getFilters(), expanded, recursive);
			filters[i].setExpanded(expanded);
		}
	}

	/**
	 * Retrieve the top level filters in the main window.
	 * 
	 * @return The top level filters.
	 */
	public Filter[] getFilters() {
		Component[] components;
		Filter[] ret;

		components = mMainPanel.getComponents();
		ret = new Filter[components.length];
		System.arraycopy(components, 0, ret, 0, components.length);

		return (ret);
	}

	/**
	 * Redo the layout.
	 */
	public void relayout() {
		mMainPanel.invalidate();
		mMainScroller.invalidate();
		mMainScroller.validate();
		mMainScroller.repaint();
	}

	/**
	 * Read a workspace from file. The current contents are erased.
	 * 
	 * @param name
	 *            The name of the file to open.
	 */
	public void open(String name) {
		LineNumberReader reader;
		String line;
		Filter[] filters;
		Point point;
		Dimension dimension;

		try {
			reader = new LineNumberReader(new FileReader(name));
			while (null != (line = reader.readLine()))
				if (line.startsWith("// [")) {
					line = line.substring(3);
					try {
						filters = Filter.reconstitute(line, new Parser(mURLField.getText()));
						mMainPanel.removeAll();
						point = new Point();
						for (int i = 0; i < filters.length; i++) {
							dimension = filters[i].getPreferredSize();
							mMainPanel.add(filters[i]);
							filters[i].setLocation(point);
							point.y += dimension.height;
						}
						setupMouseListeners(filters);
						setupDropTargets(filters);
						relayout();
					} catch (ParserException pe) {
						pe.printStackTrace();
					}
					break;
				}
			reader.close();
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}

	/**
	 * Exit back to the operating system.
	 */
	void exitApplication() {
		this.setVisible(false); // hide the Frame
		this.dispose(); // free the system resources
		System.exit(0); // close the application
	}

	/**
	 * Show a pop up context menu. Shows a context sensitive popup menu at the
	 * location of the mouse event.
	 * 
	 * @param event
	 *            The mouse event that initiates the popup.
	 */
	public void showContextMenu(MouseEvent event) {
		JPopupMenu menu;
		JMenuItem item;

		menu = new JPopupMenu();
		menu.setName("Popup");

		item = new JMenuItem("Expand");
		item.setActionCommand("expandAction");
		item.addActionListener(this);
		menu.add(item);

		item = new JMenuItem("Collapse");
		item.setActionCommand("collapseAction");
		item.addActionListener(this);
		menu.add(item);

		menu.addSeparator();

		item = new JMenuItem("Expand All");
		item.setActionCommand("expandAllAction");
		item.addActionListener(this);
		menu.add(item);

		item = new JMenuItem("CollapseAll");
		item.setActionCommand("collapseAllAction");
		item.addActionListener(this);
		menu.add(item);

		menu.addSeparator();

		item = new JMenuItem("Cut");
		item.setActionCommand("cutAction");
		item.addActionListener(this);
		menu.add(item);

		item = new JMenuItem("Copy");
		item.setActionCommand("copyAction");
		item.addActionListener(this);
		menu.add(item);

		item = new JMenuItem("Paste");
		item.setActionCommand("pasteAction");
		item.addActionListener(this);
		menu.add(item);

		item = new JMenuItem("Delete");
		item.setActionCommand("deleteAction");
		item.addActionListener(this);
		menu.add(item);

		menu.addSeparator();

		item = new JMenuItem("Execute Filter");
		item.setActionCommand("executeAction");
		item.addActionListener(this);
		menu.add(item);

		menu.show(event.getComponent(), event.getX(), event.getY());
	}

	//
	// selection manipulation
	//

	/**
	 * Add a filter to the current selection set.
	 * 
	 * @param filter
	 *            The filter to add.
	 */
	protected void addSelection(Filter filter) {
		if (!selectionContains(filter))
			mSelection.addElement(filter);
		filter.setSelected(true);
		mMoved = false;
	}

	/**
	 * Remove a filter from the current selection set.
	 * 
	 * @param filter
	 *            The filter to remove.
	 */
	protected void removeSelection(Filter filter) {
		mSelection.removeElement(filter); // no harm if not contained
		filter.setSelected(false);
	}

	/**
	 * Select(highlight)/deselect the current selection set.
	 * 
	 * @param select
	 *            If <code>true</code> turn on highlighting, turn it off
	 *            otherwise.
	 */
	protected void selectSelection(boolean select) {
		int count;
		Filter filter;

		count = mSelection.size();
		for (int i = 0; i < count; i++) {
			filter = (Filter) mSelection.elementAt(i);
			filter.setSelected(select);
		}
	}

	/**
	 * Clear (empty) the current selection set.
	 */
	protected void clearSelection() {
		selectSelection(false);
		mSelection.removeAllElements();
	}

	/**
	 * Move the current selection set as a group.
	 * 
	 * @param translation
	 *            The displacement to move them all by.
	 */
	protected void moveSelection(Point translation) {
		int count;
		Filter filter;
		Point point;

		count = mSelection.size();
		for (int i = 0; i < count; i++) {
			filter = (Filter) mSelection.elementAt(i);
			point = filter.getLocation();
			point.translate(translation.x, translation.y);
			synchronized (filter.getTreeLock()) {
				filter.setLocation(point.x, point.y);
			}
		}
		mMoved = true;
	}

	/**
	 * Check if the current selection set contains the given filter.
	 * 
	 * @param filter
	 *            The filter to check.
	 * @return <code>true</code> if the filter is a member, <code>false</code>
	 *         otherwise.
	 */
	protected boolean selectionContains(Filter filter) {
		return (mSelection.contains(filter));
	}

	/**
	 * Return the last filter added to the selection set.
	 * 
	 * @return The last filter added or <code>null</code> if the current
	 *         selection set is empty.
	 */
	protected Filter lastSelected() {
		Filter ret;

		ret = null;

		if (0 < mSelection.size())
			ret = (Filter) mSelection.lastElement();

		return (ret);
	}

	/**
	 * Return the current selection set as an array.
	 * 
	 * @return The array of selected filters.
	 */
	protected Filter[] getSelection() {
		Filter[] ret;

		ret = new Filter[mSelection.size()];
		mSelection.copyInto(ret);

		return (ret);
	}

	/**
	 * Serialize the current selection set.
	 * 
	 * @return The serialized form of the set of filters.
	 */
	public String serializeSelection() {
		Filter[] filters;
		StringWriter writer;
		PrintWriter out;

		filters = getSelection();
		writer = new StringWriter(200);
		out = new PrintWriter(writer, false);
		try {
			out.println(Filter.deconstitute(filters));
		} catch (IOException ioe) {
			ioe.printStackTrace();
		} finally {
			out.close();
		}

		return (writer.getBuffer().toString());
	}

	/**
	 * Delete the current selection set from the filters in the GUI.
	 */
	public void deleteSelection() {
		Filter[] filters;
		SubFilterList list;

		filters = getSelection();
		for (int i = 0; i < filters.length; i++) {
			list = getEnclosing(filters[i]);
			if (null != list)
				list.removeFilter(filters[i]);
			else
				mMainPanel.remove(filters[i]);
		}
		mSelection.clear();
	}

	/**
	 * Check if there is more than one filter selected.
	 * 
	 * @return <code>true</code> if only one filter is selected,
	 *         <code>false</code> otherwise.
	 */
	public boolean isSingleSelection() {
		return (1 == mSelection.size());
	}

	//
	// MouseListener interface
	//

	/**
	 * Invoked when the mouse has been clicked on a component.
	 * 
	 * @param event
	 *            The mouse clicked event.
	 */
	public void mouseClicked(MouseEvent event) {
		Object component;
		Filter filter;
		SubFilterList list;
		int modifiers;
		boolean contained;
		Filter[] filters;

		component = event.getSource();
		if (component instanceof Filter) {
			filter = (Filter) component;
			modifiers = event.getModifiers();
			contained = selectionContains(filter);

			if (0 != (modifiers & InputEvent.SHIFT_MASK)) {
				// add everything from last selected to this command
				list = getEnclosed(filter);
				if (null != list)
					filters = list.getFilters();
				else
					filters = getFilters();
				Filter last = lastSelected();
				if (null == last)
					addSelection(filter);
				else {
					int current = -1;
					int recent = -1;
					for (int i = 0; i < filters.length; i++) {
						if (filters[i] == filter)
							current = i;
						if (filters[i] == last)
							recent = i;
					}
					if ((current != -1) && (recent != -1))
						for (int i = Math.min(current, recent); i <= Math.max(current, recent); i++)
							addSelection(filters[i]);
				}
			} else if (0 != (modifiers & InputEvent.CTRL_MASK)) {
				// add just the new command
				if (contained)
					removeSelection(filter);
				else
					addSelection(filter);
			} else if (0 != (modifiers & InputEvent.BUTTON3_MASK)) {
				if (!selectionContains(filter)) {
					clearSelection();
					addSelection(filter);
				}
				showContextMenu(event);
			} else {
				clearSelection();
				addSelection(filter);
			}
		} else
			clearSelection();
	}

	/**
	 * Invoked when a mouse button has been released on a component.
	 * 
	 * @param event
	 *            The mouse released event.
	 */
	public void mouseReleased(MouseEvent event) {
	}

	/**
	 * Invoked when the mouse enters a component.
	 * 
	 * @param event
	 *            The mouse entered event.
	 */
	public void mouseEntered(MouseEvent event) {
	}

	/**
	 * Invoked when the mouse exits a component.
	 * 
	 * @param event
	 *            The mouse exited event.
	 */
	public void mouseExited(MouseEvent event) {
	}

	/**
	 * Invoked when a mouse button has been pressed on a component.
	 * 
	 * @param event
	 *            The mouse pressed event.
	 */
	public void mousePressed(MouseEvent event) {
		Object component;
		Point newpoint;
		Point upperleft;

		component = event.getSource();
		if (component instanceof Filter) {
			// translate the point relative to the enclosing container
			newpoint = event.getPoint();
			upperleft = ((Component) component).getLocation();
			newpoint.translate(upperleft.x, upperleft.y);
			setBasePoint(newpoint);
		} else
			setBasePoint(null);
	}

	//
	// MouseMotionListener interface
	//

	/**
	 * Mouse drag notification. Invoked when a mouse button is pressed on a
	 * component and then dragged. Mouse drag events will continue to be
	 * delivered to the component where the first originated until the mouse
	 * button is released (regardless of whether the mouse position is within
	 * the bounds of the component).
	 * 
	 * @param event
	 *            The mouse drag event.
	 */
	public synchronized void mouseDragged(MouseEvent event) {
		Object component;
		Filter filter;
		Point base;
		Point newpoint;
		Point upperleft;
		Point translation;

		component = event.getSource();
		if (component instanceof Filter) {
			filter = (Filter) component;
			if (selectionContains(filter)) // drag on a selected item
			{
				if (null == getEnclosing(filter)) // not contained
					try {
						base = getBasePoint();
						if (null != base) {
							newpoint = event.getPoint();
							// translate the point relative to the enclosing
							// container
							upperleft = filter.getLocation();
							newpoint.translate(upperleft.x, upperleft.y);
							// get the difference between this point and the old
							// base
							translation = new Point(newpoint.x - base.x, newpoint.y - base.y);
							// update the base point
							setBasePoint(newpoint);
							// apply this difference to the selection
							moveSelection(translation);
						}
					} catch (Exception e) {
					}
			} else
				mouseClicked(event); // a small slip shouldn't stop a click
		}
	}

	/**
	 * Mouse move notification. Invoked when the mouse button has been moved on
	 * a component (with no buttons no down).
	 * 
	 * @param event
	 *            The mouse moved event.
	 */
	public void mouseMoved(MouseEvent event) {
	}

	//
	// WindowListener interface
	//

	/**
	 * Invoked the first time a window is made visible. <i>Not used.</i>
	 * 
	 * @param event
	 *            The window event.
	 */
	public void windowOpened(WindowEvent event) {
	}

	/**
	 * Handles window closing event. Performs function
	 * <code>exitApplication()</code>.
	 * 
	 * @param event
	 *            The window event.
	 */
	public void windowClosing(WindowEvent event) {
		if (event.getSource() == this)
			exitApplication();
	}

	/**
	 * Invoked when a window has been closed as the result of calling dispose on
	 * the window. <i>Not used.</i>
	 * 
	 * @param event
	 *            The window event.
	 */
	public void windowClosed(WindowEvent event) {
	}

	/**
	 * Invoked when a window is changed from a normal to a minimized state. For
	 * many platforms, a minimized window is displayed as the icon specified in
	 * the window's iconImage property. <i>Not used.</i>
	 * 
	 * @param event
	 *            The window event.
	 */
	public void windowIconified(WindowEvent event) {
	}

	/**
	 * Invoked when a window is changed from a minimized to a normal state.
	 * <i>Not used.</i>
	 * 
	 * @param event
	 *            The window event.
	 */
	public void windowDeiconified(WindowEvent event) {
	}

	/**
	 * Invoked when the window is set to be the user's active window, which
	 * means the window (or one of its subcomponents) will receive keyboard
	 * events. <i>Not used.</i>
	 * 
	 * @param event
	 *            The window event.
	 */
	public void windowActivated(WindowEvent event) {
	}

	/**
	 * Invoked when a window is no longer the user's active window, which means
	 * that keyboard events will no longer be delivered to the window or its
	 * subcomponents. <i>Not used.</i>
	 * 
	 * @param event
	 *            The window event.
	 */
	public void windowDeactivated(WindowEvent event) {
	}

	//
	// ActionListener interface
	//

	/**
	 * Handles menu and toolbar item choices.
	 * 
	 * @param event
	 *            The action even that triggers this function.
	 */
	public void actionPerformed(ActionEvent event) {
		Object object;
		String action;

		object = event.getSource();
		// if (object instanceof JButton)
		// {
		// String url;
		// url = mURLField.getText ();
		// mURLField.selectAll ();
		// //setURL (url);
		// }
		if (object instanceof JButton)
			action = ((JButton) object).getActionCommand();
		else if (object instanceof JMenuItem)
			action = ((JMenuItem) object).getActionCommand();
		else
			action = null;

		if (object instanceof Component)
			mCurrentComponent = (Component) object;

		if (null != action)
			try {
				Method method = this.getClass().getDeclaredMethod(action, new Class[0]);
				method.invoke(this, new Object[0]);
			} catch (NoSuchMethodException nsme) {
				System.out.println("no " + action + " method found");
			} catch (Exception e) {
				e.printStackTrace();
			}
	}

	//
	// ClipboardOwner interface
	//

	/**
	 * Notifies this object that it is no longer the owner of the contents of
	 * the clipboard.
	 * 
	 * @param clipboard
	 *            The clipboard that is no longer owned.
	 * @param contents
	 *            The contents which this owner had placed on the clipboard.
	 */
	public void lostOwnership(Clipboard clipboard, Transferable contents) {
		System.out.println("lost clipboard ownership");
	}

	//
	// DragGestureListener interface
	//

	/**
	 * A DragGestureRecognizer has detected a platform-dependent drag initiating
	 * gesture. It is notifying this listener in order for it to initiate the
	 * action for the user.
	 * 
	 * @param event
	 *            The DragGestureEvent describing the gesture that has just
	 *            occurred.
	 */
	public void dragGestureRecognized(DragGestureEvent event) {
		Component component;
		String cls;
		Filter filter;
		StringSelection text;

		component = event.getComponent();
		try {
			cls = component.getName(); // (String)Filter.mWrappers.get
										// (component.getName ());
			if (null != cls) {
				filter = Filter.instantiate(cls);
				text = new StringSelection(Filter.deconstitute(new Filter[] { filter }));
				mDragSource.startDrag(event, DragSource.DefaultMoveDrop, text, this);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	//
	// DragSourceListener interface
	//

	/**
	 * This message goes to DragSourceListener, informing it that the dragging
	 * has ended.
	 * 
	 * @param event
	 *            Details about the drop event.
	 */
	public void dragDropEnd(DragSourceDropEvent event) {
		if (event.getDropSuccess()) {
			// System.out.println ("added new class");
		}
	}

	/**
	 * This message goes to DragSourceListener, informing it that the dragging
	 * has entered the DropSite.
	 * 
	 * @param event
	 *            Details about the drag event.
	 */
	public void dragEnter(DragSourceDragEvent event) {
		// System.out.println ("dragEnter");
	}

	/**
	 * This message goes to DragSourceListener, informing it that the dragging
	 * has exited the DropSite.
	 * 
	 * @param event
	 *            Details about the drag event.
	 */
	public void dragExit(DragSourceEvent event) {
		// System.out.println( "dragExit");
	}

	/**
	 * This message goes to DragSourceListener, informing it that the dragging
	 * is currently ocurring over the DropSite.
	 * 
	 * @param event
	 *            Details about the drag event.
	 */
	public void dragOver(DragSourceDragEvent event) {
		// System.out.println( "dragExit");
	}

	/**
	 * This is invoked when the user changes the dropAction.
	 * 
	 * @param event
	 *            Details about the drop action event.
	 */
	public void dropActionChanged(DragSourceDragEvent event) {
		// System.out.println( "dropActionChanged");
	}

	//
	// DropTargetListener interface
	//

	/**
	 * This is invoked when you are dragging over the DropSite.
	 * 
	 * @param event
	 *            Details about the drag event.
	 */
	public void dragEnter(DropTargetDragEvent event) {
		// debug messages for diagnostics
		// event.acceptDrag (DnDConstants.ACTION_MOVE);
		// System.out.println ("dragEnter");
		DropTargetContext context;
		Component component;
		SubFilterList list;

		// find the enclosing filter
		context = event.getDropTargetContext();
		component = context.getComponent();
		while ((null != component) && !(component instanceof SubFilterList) && !(component == mMainPanel))
			component = component.getParent();
		if (component instanceof SubFilterList)
			list = (SubFilterList) component;
		else
			list = null;
		// so either list is the enclosing list,
		// or list is null and the target component is the main panel

		if (null != list)
			if (!list.canAccept())
				event.rejectDrag();
			else
				list.setSelected(true);
	}

	/**
	 * Thi ss invoked when you are exit the DropSite without dropping.
	 * 
	 * @param event
	 *            Details about the drag event.
	 */
	public void dragExit(DropTargetEvent event) {
		// debug messages for diagnostics
		// event.acceptDrag (DnDConstants.ACTION_MOVE);
		// System.out.println ("dragEnter");
		DropTargetContext context;
		Component component;
		SubFilterList list;

		// find the enclosing filter
		context = event.getDropTargetContext();
		component = context.getComponent();
		while ((null != component) && !(component instanceof SubFilterList) && !(component == mMainPanel))
			component = component.getParent();
		if (component instanceof SubFilterList)
			list = (SubFilterList) component;
		else
			list = null;
		// so either list is the enclosing list,
		// or list is null and the target component is the main panel

		if (null != list)
			list.setSelected(false);
	}

	/**
	 * This is invoked when a drag operation is going on.
	 * 
	 * @param event
	 *            Details about the drag event.
	 */
	public void dragOver(DropTargetDragEvent event) {
		// System.out.println( "dragOver");
	}

	/**
	 * This is invoked when a drop has occurred.
	 * 
	 * @param event
	 *            The drop event.
	 */
	public void drop(DropTargetDropEvent event) {
		DropTargetContext context;
		Component component;
		SubFilterList list;
		String s;
		Point point;
		Filter[] filters;
		boolean accept;

		// find the enclosing filter
		context = event.getDropTargetContext();
		component = context.getComponent();
		while ((null != component) && !(component instanceof SubFilterList) && !(component == mMainPanel))
			component = component.getParent();
		if (component instanceof SubFilterList)
			list = (SubFilterList) component;
		else
			list = null;
		// so either list is the enclosing list,
		// or list is null and the target component is the main panel

		try {
			accept = false;
			Transferable transferable = event.getTransferable();

			// we accept only Strings
			if (transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) {
				accept = true;
				event.acceptDrop(DnDConstants.ACTION_MOVE);
				s = (String) transferable.getTransferData(DataFlavor.stringFlavor);
				point = event.getLocation();
				try {
					// get the filter and add into the target
					filters = Filter.reconstitute(s, new Parser(mURLField.getText()));
					if (0 < filters.length)
						insertFilters(filters, point, list);
					if (null != list)
						list.setSelected(false);
				} catch (Exception e) {
					e.printStackTrace();
				}
				// signal the drop was successful
				context.dropComplete(accept);
			} else
				event.rejectDrop();
		} catch (IOException exception) {
			exception.printStackTrace();
			System.err.println("Exception" + exception.getMessage());
			event.rejectDrop();
		} catch (UnsupportedFlavorException ufException) {
			ufException.printStackTrace();
			System.err.println("Exception" + ufException.getMessage());
			event.rejectDrop();
		}
	}

	/**
	 * This is invoked if the user modifies the current drop gesture.
	 * 
	 * @param event
	 *            Details about the drop action change event.
	 */
	public void dropActionChanged(DropTargetDragEvent event) {
		// System.out.println( "dropActionChanged");
	}

	/**
	 * The entry point for this application. Creates a new FilterBuilder and
	 * makes it visible.
	 * 
	 * @param args
	 *            [0] optional URL to operate on.
	 */
	public static void main(String args[]) {
		try {
			// set the Look and Feel to the the native system
			// try
			// {
			// javax.swing.UIManager.setLookAndFeel
			// (javax.swing.UIManager.getSystemLookAndFeelClassName ());
			// }
			// catch (Exception e)
			// {
			// }

			// create a new instance of our application's frame, and make it
			// visible
			FilterBuilder builder = new FilterBuilder();
			if (0 != args.length)
				builder.mURLField.setText(args[0]);
			builder.setVisible(true);
		} catch (Throwable t) {
			t.printStackTrace();
			// ensure the application exits with an error condition
			System.exit(1);
		}
	}

}
